/* Track Panel
 *
 * Created : Jun 14, 2012
 *
 * @author pquiring
 */

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

import javaforce.*;
import javaforce.media.*;

public class TrackPanel extends javax.swing.JPanel {

  /**
   * Creates new form TrackPanel
   */
  public TrackPanel(ProjectPanel project, int tid, Wav wav) {
    initComponents();
    setLayout(new TrackLayout());
    this.project = project;
    this.tid = tid;
    new File(project.path + "/" + tid).mkdir();
    importWav(wav);
    initForms();
  }

  public TrackPanel(ProjectPanel project, int tid, int chs, int rate, int bits) {
    initComponents();
    setLayout(new TrackLayout());
    this.project = project;
    this.tid = tid;
    this.channels = chs;
    this.rate = rate;
    this.bits = bits;
    bytes = bits / 8;
    new File(project.path + "/" + tid).mkdir();
    writeMainHeader();
    initForms();
  }

  public TrackPanel(ProjectPanel project, int tid) {
    initComponents();
    setLayout(new TrackLayout());
    this.project = project;
    this.tid = tid;
    new File(project.path + "/" + tid).mkdir();
    loadChunks();
    initForms();
  }

  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGap(0, 626, Short.MAX_VALUE)
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGap(0, 83, Short.MAX_VALUE)
    );
  }// </editor-fold>//GEN-END:initComponents
  // Variables declaration - do not modify//GEN-BEGIN:variables
  // End of variables declaration//GEN-END:variables

  public ProjectPanel project;
  public int tid = -1;
  public int channels = -1;
  public int bits = -1;
  public int bytes = -1;
  public int rate = -1;
  public int first_cid = 0;
  public long totalLength = 0;  //in samples
  public static final int lowRes = 256;  //# of samples lowRes per hiRes
  public static final int maxChunkSize = 256 * 1024; //in samples
  public long selectStart, selectStop;  //in samples
  public volatile boolean muted = false;
  public boolean selected = false;

  public int undoAction1;  //ACTION_CUT / MODIFY / PASTE
  public long undoAction1OffsetStart;
  public long undoAction1OffsetStop;
  public int undoAction2;  //ACTION_PASTE only
  public long undoAction2OffsetStart;
  public long undoAction2OffsetStop;
  public int undoRate = 0;

  public static final int ACTION_NA = 0;
  public static final int ACTION_CUT = 1;
  public static final int ACTION_MODIFY = 2;
  public static final int ACTION_PASTE = 3;

  public static class MainHeader {
    //all chunks must be the same format as this
    public int channels;
    public int bits;
    public int bytes;
    public int rate;
    public int first_cid;  //first chunk id
    public void write(OutputStream os) throws Exception {
      JF.writeuint32(os, channels);
      JF.writeuint32(os, bits);
      JF.writeuint32(os, bytes);
      JF.writeuint32(os, rate);
      JF.writeuint32(os, first_cid);
    }
    public void read(InputStream is) throws Exception {
      channels = JF.readuint32(is);
      bits = JF.readuint32(is);
      bytes = JF.readuint32(is);
      rate = JF.readuint32(is);
      first_cid = JF.readuint32(is);
    }
    public void write(RandomAccessFile raf) throws Exception {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      write(baos);
      raf.write(baos.toByteArray());
    }
  }

  /** ClipHeader - used for undo and clipboard */
  public static class ClipHeader {
    public int tid;
    public long offset, length;
    public int channels;
    public int bits;
    public int bytes;
    public int rate;
    public void write(OutputStream os) throws Exception {
      JF.writeuint32(os, tid);
      JF.writeuint64(os, offset);
      JF.writeuint64(os, length);
      JF.writeuint32(os, channels);
      JF.writeuint32(os, bits);
      JF.writeuint32(os, bytes);
      JF.writeuint32(os, rate);
    }
    public void read(InputStream is) throws Exception {
      tid = JF.readuint32(is);
      offset = JF.readuint64(is);
      length = JF.readuint64(is);
      channels = JF.readuint32(is);
      bits = JF.readuint32(is);
      bytes = JF.readuint32(is);
      rate = JF.readuint32(is);
    }
  }

  private static final int chunkHeaderLength = 8;

  public static class ChunkHeader {
    public int cid;  //not written to disk
    public int length;  //in samples
    public int next_cid;  //-1 = last chunk
    public Object lock = new Object();  //not written to disk
    public void write(OutputStream os) throws Exception {
      JF.writeuint32(os, length);
      JF.writeuint32(os, next_cid);
    }
    public void write(RandomAccessFile raf) throws Exception {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      write(baos);
      raf.write(baos.toByteArray());
    }
    public void read(InputStream is) throws Exception {
      length = JF.readuint32(is);
      next_cid = JF.readuint32(is);
    }
    public void read(RandomAccessFile raf) throws Exception {
      byte tmp[] = new byte[chunkHeaderLength];
      raf.read(tmp);
      ByteArrayInputStream bais = new ByteArrayInputStream(tmp);
      read(bais);
    }
  }

  private ArrayList<ChunkHeader> list = new ArrayList<ChunkHeader>();
  private WaveForm forms[];

  private void loadChunks() {
    try {
      MainHeader main = new MainHeader();
      FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/track.dat");
      main.read(fis);
      this.channels = main.channels;
      this.bits = main.bits;
      this.bytes = main.bytes;
      this.rate = main.rate;
      int cid = main.first_cid;
      fis.close();
      while (cid != -1) {
        fis = new FileInputStream(project.path + "/" + tid + "/c" + cid + "-0.dat");
        ChunkHeader chunk = new ChunkHeader();
        chunk.read(fis);
        fis.close();
        list.add(chunk);
        totalLength += chunk.length;
        if (chunk.next_cid == cid) throw new Exception("chunk sequence error");
        cid = chunk.next_cid;
      }
    } catch (Exception e) {
      JFLog.log(e);
    }
  }

  public void writeMainHeader() {
    try {
      MainHeader main = new MainHeader();
      main.channels = channels;
      main.rate = rate;
      main.bits = bits;
      main.bytes = bytes;
      main.first_cid = first_cid;
      RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/track.dat", "rw");
      main.write(raf);
      raf.close();
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
  private void importWav(Wav wav) {
    JFLog.log("importing wav");
    MainHeader main = new MainHeader();
    main.channels = wav.chs;
    main.rate = wav.rate;
    main.bits = wav.bits;
    main.bytes = wav.bytes;
    if (main.bits == 24) {
      //must upgrade to 32bits, there is no 24bit integer
      main.bits = 32;
      main.bytes = 4;
    }
    channels = main.channels;
    rate = main.rate;
    bits = main.bits;
    bytes = main.bytes;
    writeMainHeader();
    long length = wav.dataLength/wav.bytes/wav.chs;  //in samples
    totalLength = length;
    JFTask task = new JFTask() {
      public boolean work() {
        Wav wav = (Wav)this.getProperty("wav");
        this.setLabel("Importing file...");
        this.setTitle("Progress");
        try {
          long toRead = totalLength;
          int cid = 0;
          byte samples[], chSamples[];
          while (toRead > 0) {
            this.setProgress((int)((totalLength - toRead) * 100 / totalLength));
            if (abort) return true;
            int read = maxChunkSize;
            if (read > toRead) read = (int)toRead;
            samples = wav.readSamples(read);
            ChunkHeader chunk = new ChunkHeader();
            chunk.cid = cid;
            chunk.length = read;
            if (read == toRead) {
              chunk.next_cid = -1;
            } else {
              chunk.next_cid = cid+1;
            }
            chSamples = new byte[read * bytes];
            for(int ch=0;ch<channels;ch++) {
              int dst, src;
              for(int s=0;s<read;s++) {
                src = s * channels * bytes + ch * bytes;
                switch (bits) {
                  case 16:
                    dst = s * 2;
                    chSamples[dst + 0] = samples[src + 0];
                    chSamples[dst + 1] = samples[src + 1];
                    break;
                  case 32:
                    dst = s * 4;
                    chSamples[dst + 0] = samples[src + 0];
                    chSamples[dst + 1] = samples[src + 1];
                    chSamples[dst + 2] = samples[src + 2];
                    chSamples[dst + 3] = samples[src + 3];
                    break;
                }
              }
              FileOutputStream fos = new FileOutputStream(project.path + "/" + tid + "/c" + cid + "-" + ch + ".dat");
              chunk.write(fos);
              fos.write(chSamples);
              fos.close();
              genLowResChunk(chSamples, chunk, ch);
            }
            list.add(chunk);
            toRead -= read;
            cid++;
          }
          JFLog.log("import complete");
          project.calcMaxLength();
          project.autoZoom();
          repaint();
        } catch (Exception e) {
          JFLog.log(e);
          JFAWT.showError("Error", e.toString());
        }
        return true;
      }
    };
    task.setProperty("wav", wav);
    ProgressDialog dialog = new ProgressDialog(null, true, task);
    dialog.setAutoClose(true);
    dialog.setVisible(true);
  }

  private void genLowResChunk(byte samples[], ChunkHeader hiResChunk, int ch) throws Exception {
    //creates a low-res 8bit copy of a chunk (lowRes samples average)
    FileOutputStream fos = new FileOutputStream(project.path + "/" + tid + "/a" + hiResChunk.cid + "-" + ch + ".dat");
    ChunkHeader chunk = new ChunkHeader();
    chunk.cid = hiResChunk.cid;
    chunk.length = hiResChunk.length;
    chunk.next_cid = hiResChunk.next_cid;
    chunk.write(fos);
    int len = chunk.length / lowRes;
    if (len == 0) return;
    byte avg[] = new byte[len];
    switch (bits) {
      case 16:
        short samples16[] = LE.byteArray2shortArray(samples, null);
        for(int a=0;a<len;a++) {
          short val = 0, max = 0;
          int pos = a * lowRes;
          for(int b=0;b<lowRes;b++) {
            val = (short)Math.abs(samples16[pos + b]);
            if (val > max) max = val;
          }
          max >>= 8;
          avg[a] = (byte)max;
        }
        break;
      case 32:
        int samples32[] = LE.byteArray2intArray(samples, null);
        for(int a=0;a<len;a++) {
          int val = 0, max = 0;
          int pos = a * lowRes;
          for(int b=0;b<lowRes;b++) {
            val = Math.abs(samples32[pos + b]);
            if (val > max) max = val;
          }
          max >>= 24;
          avg[a] = (byte)max;
        }
        break;
    }
    fos.write(avg);
    fos.close();
  }

  private boolean showChunks = false;  //for testing

  private class WaveForm extends JComponent implements MouseListener, MouseMotionListener {
    public int channel;
    public WaveForm(int channel) {
      this.channel = channel;
    }
    public void paint(Graphics g) {
      //clear background (muted = black, normal = gray)
      if (muted)
        g.setColor(Color.BLACK);
      else
        g.setColor(Color.GRAY);
      g.fillRect(0,0,getWidth(),getHeight());
      double rateScale = (rate / project.scale);
      //paint selection (dark gray)
      g.setColor(Color.DARK_GRAY);
      int sStart = (int)((selectStart - project.offset * rate) / (rate / project.scale));
      int sStop = (int)((selectStop - project.offset * rate) / (rate / project.scale));
      if (sStart == sStop) {
        sStop++;
      }
//      JFLog.log("start:" + sStart + ",stop:" + sStop);
      if (sStart < sStop) {
        g.fillRect(sStart, 0, sStop - sStart, getHeight());
      } else {
        g.fillRect(sStop, 0, sStart - sStop, getHeight());
      }
      //paint selected border
      if (selected) {
        g.setColor(Color.YELLOW);
        g.drawRect(0,0,getWidth()-1,getHeight()-1);
      }
      //paint waveForm
      if (rateScale >= lowRes) {
        paintLowRes(g);
      } else {
        paintHiRes(g);
      }
    }
    public void paintHiRes(Graphics g) {
      double rateScale = (rate / project.scale);
      int intRateScale = (int)rateScale;
//      JFLog.log("paintHiRes:offset=" + project.offset + ":rateScale=" + rateScale + ":int=" + intRateScale + ":channel=" + channel);
      double rateScaleDec = (rate / project.scale) - (double)intRateScale;
//      JFLog.log("rateScaleDec=" + rateScaleDec);
      double rateScaleCnt;
      int length = getWidth();  //in px
      long start = (long)(project.offset * rate);  //in samples
      long end = (long)(start + (length * rateScale));  //in samples
      long skip = start;  //in samples
      int pos = 0;
      g.setColor(Color.BLUE);
      int cid = first_cid;
      try {
        int listSize = list.size();
        int px = 0;
        for(int lidx=0;lidx<listSize;lidx++) {
          boolean first = showChunks;
          if (pos == length) break;
          ChunkHeader chunk = list.get(lidx);
          synchronized(chunk.lock) {
            if (chunk.length <= skip) {
              skip -= chunk.length;
              continue;
            }
            rateScaleCnt = 0.0;
  //          JFLog.log("chunk.length=" + chunk.length);
            FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/c" + chunk.cid + "-" + channel + ".dat");
            fis.skip(chunkHeaderLength);
            long chunkLength = chunk.length;
            if (skip > 0) {
              chunkLength -= skip;
              fis.skip(skip * bytes);
              skip = 0;
            }
            int extraSample;
            while (chunkLength > 0) {
              rateScaleCnt += rateScaleDec;
              if (rateScaleCnt >= 1.0) {
                rateScaleCnt -= 1.0;
                extraSample = 1;
              } else {
                extraSample = 0;
              }
              int samplesToRead = (intRateScale + extraSample);
              if (samplesToRead > 0) {
                if (samplesToRead > chunkLength) samplesToRead = (int)chunkLength;
                byte samples[] = new byte[samplesToRead * bytes];
                JF.readAll(fis, samples, 0, samples.length);
                int max = 0;
                switch (bits) {
                  case 16:
                    short samples16[] = LE.byteArray2shortArray(samples, null);
                    for(int a=0;a<samples16.length;a++) {
                      int val = Math.abs(samples16[a]);
                      if (val > max) max = val;
                    }
                    max >>= 8;
                    break;
                  case 32:
                    int samples32[] = LE.byteArray2intArray(samples, null);
                    for(int a=0;a<samples32.length;a++) {
                      int val = Math.abs(samples32[a]);
                      if (val > max) max = val;
                    }
                    max >>= 24;
                    break;
                }
                px = (byte)(max >> 1);
              }
              if (first) {
                g.setColor(Color.RED);
                px = 63;
              }
              g.drawLine(pos, 64 - px, pos, 64 + px);
              if (first) {
                g.setColor(Color.BLUE);
                first = false;
              }
              pos++;
              if (pos == length) break;
              chunkLength -= samplesToRead;
            }
  //          JFLog.log("last.pos=" + pos);
            fis.close();
          }
        }
      } catch (Exception e) {
        JFLog.log(e);
      }
    }
    public void paintLowRes(Graphics g) {
      //basically same as paint() except lowResRate = rate/lowRes; bytes=1;
//      JFLog.log("paintLowRes:scale=" + project.scale);
      double rateScale = (rate / project.scale);
      double lowResRate = rate / lowRes;
      double lowResRateScale = (lowResRate / project.scale);
      int intLowResRateScale = (int)lowResRateScale;
      double lowResRateScaleDec = (lowResRate / project.scale) - (double)intLowResRateScale;
      double lowResRateScaleCnt;
      int length = getWidth();  //in px
      long start = (long)(project.offset * lowResRate);  //in samples/lowRes
      long end = (long)(start + (length * lowResRateScale));  //in samples/lowRes
      long skip = start;  //in samples/lowRes
      int pos = 0;
      g.setColor(Color.BLUE);
      try {
        int listSize = list.size();
        for(int lidx=0;lidx<listSize;lidx++) {
          boolean first = showChunks;
          if (pos == length) break;
          ChunkHeader chunk = list.get(lidx);
          synchronized(chunk.lock) {
            if (chunk.length / lowRes <= skip) {
              skip -= chunk.length / lowRes;
              continue;
            }
            lowResRateScaleCnt = 0.0;
            FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/a" + chunk.cid + "-" + channel + ".dat");
            fis.skip(chunkHeaderLength);
            long chunkLength = chunk.length / lowRes;
            if (skip > 0) {
              chunkLength -= skip;
              fis.skip(skip);
              skip = 0;
            }
            int extraSample;
            while (chunkLength > 0) {
              lowResRateScaleCnt += lowResRateScaleDec;
              if (lowResRateScaleCnt >= 1.0) {
                lowResRateScaleCnt -= 1.0;
                extraSample = 1;
              } else {
                extraSample = 0;
              }
              int samplesToRead = intLowResRateScale + extraSample;
              if (samplesToRead > chunkLength) samplesToRead = (int)chunkLength;
              byte samples[] = new byte[samplesToRead];
              JF.readAll(fis, samples, 0, samplesToRead);
              int px;
              int max = 0;
              for(int a=0;a<samples.length;a++) {
                int val = samples[a];
                if (val > max) max = val;
              }
              px = (byte)(max >> 1);
              if (first) {
                g.setColor(Color.RED);
                px = 63;
              }
              g.drawLine(pos, 64 - px, pos, 64 + px);
              if (first) {
                g.setColor(Color.BLUE);
                first = false;
              }
              pos++;
              if (pos == length) break;
              chunkLength -= samplesToRead;
            }
            fis.close();
          }
        }
      } catch (Exception e) {
        JFLog.log(e);
      }
    }
    //height = 63+1+63 = 127
    public Dimension getPreferredSize() {
      return new Dimension(project.getTracksWidth(), 127);
    }
    public Dimension getMaximumSize() {
      return getPreferredSize();
    }

    public void mouseClicked(MouseEvent me) {
    }

    public void mousePressed(MouseEvent me) {
      double timeOffset = (((double)me.getX()) / ((double)project.scale)) + project.offset;
      project.selectStart(timeOffset);
      double rateScale = rate / project.scale;
      selectStart = (long)(((double)me.getX()) * (rateScale) + project.offset * ((double)rate));
      if (selectStart < 0) selectStart = 0;
      selectStop = selectStart;
      selectTrack(true);
      repaint();
    }

    public void mouseReleased(MouseEvent me) {
      double timeOffset = me.getX() / project.scale + project.offset;
      project.selectStop(timeOffset);
      selectStop = (long)(me.getX() * (rate / project.scale) + project.offset * rate);
      if (selectStop < 0) selectStop = 0;
      repaint();
    }

    public void mouseEntered(MouseEvent me) {
    }

    public void mouseExited(MouseEvent me) {
    }

    public void mouseDragged(MouseEvent me) {
      mouseReleased(me);
    }

    public void mouseMoved(MouseEvent me) {
    }
  }
  private void initForms() {
    forms = new WaveForm[channels];
    for(int a=0;a<channels;a++) {
      forms[a] = new WaveForm(a);
      forms[a].addMouseListener(forms[a]);
      forms[a].addMouseMotionListener(forms[a]);
      add(forms[a]);
    }
  }
  public Dimension getPreferredSize() {
    return new Dimension(project.getWidth(),127 * channels);
  }
  public Dimension getMaximumSize() {
    return getPreferredSize();
  }
  public void swapEndian(int bits, byte samples[]) {
    byte tmp;
    int len = samples.length;
    switch (bits) {
      case 16:
        for(int a=0;a<len;a+=2) {
          tmp = samples[a];
          samples[a] = samples[a+1];
          samples[a+1] = tmp;
        }
        break;
      case 32:
        for(int a=0;a<len;a+=4) {
          tmp = samples[a];
          samples[a] = samples[a+3];
          samples[a+3] = tmp;
          tmp = samples[a+1];
          samples[a+1] = samples[a+2];
          samples[a+2] = tmp;
        }
        break;
    }
  }
  private void scaleBufferVolume(byte buf[], int bits, int scale) {
    int len = buf.length;
    short s16;
    int s32;
    if (scale == 0) {
      for (int a = 0; a < len; a++) {
        buf[a] = 0;
      }
    } else {
      switch (bits) {
        case 16:
          float fscale = (float)scale / 100f;
          for (int a = 0; a < len; a+=2) {
            s16 = (short)LE.getuint16(buf, a);
            s16 *= fscale;
            LE.setuint16(buf, a, s16);
          }
          break;
        case 32:
          double dscale = (double)scale / 100d;
          for (int a = 0; a < len; a+=4) {
            s32 = LE.getuint32(buf, a);
            s32 *= dscale;
            LE.setuint32(buf, a, s32);
          }
          break;
      }
    }
  }
  public volatile boolean recording = false;
  public volatile boolean playing = false;
  private static final int recBufSize = 256;  //in bytes (not samples)
  public void record() {
    selectTrack(true);
    this.rate = Settings.current.freq;
    recording = true;
    new Thread() {
      public void run() {
        long length = 0;
        AudioInput input = new AudioInput();
        input.listDevices();
        if (!input.start(Settings.current.channels, Settings.current.freq, bits, recBufSize, Settings.getInput())) {
          MainPanel.main.stop();
          JFAWT.showError("Error", "Failed to open recording device.");
          return;
        }
        byte samples[] = new byte[recBufSize];
        while (recording) {
          if (!input.read(samples)) {
            JF.sleep(10);
            continue;
          }
          swapEndian(bits, samples);
          int lvl = MainPanel.main.getRecordLevel();
          if (lvl < 100) {
            scaleBufferVolume(samples, bits, lvl);
          }
          addSamples(samples);
          length += samples.length / channels / bytes;
          project.calcMaxLength();
          project.showOffset(length / rate);
          repaint();
        }
        input.stop();
      }
    }.start();
  }
  public final int playBufferSize = 4 * 1024;  //keep small for better response (in samples)
  public void play() {
    playing = true;
    new Thread() {
      public void run() {
        int bufferSize = playBufferSize * bytes * channels;
        AudioOutput output = new AudioOutput();
        output.listDevices();
        long offset = selectStart;
        boolean first = true;
        int firstNumSamples = 0;
        int numSamples;
        output.start(channels, rate, bits, bufferSize, Settings.getOutput());
        while (playing) {
          synchronized(project.pausedLock) {
            if (project.paused) {
              try {project.pausedLock.wait();} catch (Exception e) {}
            }
          }
          byte samples[] = getSamples(offset, playBufferSize);
          if (samples == null) break;
          if (samples.length != bufferSize) break;
          if (muted) {
            Arrays.fill(samples, (byte)0);
          }
          numSamples = samples.length / channels / bytes;
          int lvl = MainPanel.main.getPlayLevel();
          if (lvl != 100) {
            scaleBufferVolume(samples, bits, lvl);
          }
          swapEndian(bits, samples);
          output.write(samples);
          offset += playBufferSize;
          if (first) {first = false; firstNumSamples = numSamples; continue;}
          JF.sleep(numSamples * 1000 / rate);
        }
        JF.sleep(firstNumSamples * 1000 / rate * 2);  //*2 for the delay in starting
        output.stop();
        playing = false;
        project.stopped(tid);
      }
    }.start();
  }
  public void stopRecording() {
    recording = false;
  }
  public void stopPlaying() {
    playing = false;
  }
  public void stop() {
    if (recording) stopRecording(); else stopPlaying();
  }
  /** Adds samples to the end of this track. */
  public void addSamples(byte data[]) {
    //check last chunk
    int samplesLength = data.length / channels / bytes;
    totalLength += samplesLength;
    int cid;  //chunk id
    ChunkHeader chunk = new ChunkHeader();
    ChunkHeader lastChunk;
    if (list.size() > 0) {
      lastChunk = list.get(list.size() - 1);
      if (lastChunk.length + samplesLength <= maxChunkSize) {
        //add to last chunk
        synchronized(lastChunk.lock) {
          lastChunk.length += samplesLength;
          try {
            cid = list.size() - 1;  //last chunk
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            lastChunk.write(baos);
            for(int ch=0;ch<channels;ch++) {
              byte samples[] = sliceSamples(data, ch);
              RandomAccessFile file = new RandomAccessFile(project.path + "/" + tid + "/c" + cid + "-" + ch + ".dat", "rw");
              int fileLength = (int)file.length();
              byte header[] = baos.toByteArray();
              file.seek(0);
              file.write(header);
              file.seek(fileLength);
              file.write(samples);
              int allLength = fileLength - header.length + samples.length;
              byte allSamples[] = new byte[allLength];
              file.seek(header.length);
              JF.readAll(file, allSamples, 0, fileLength - header.length);
              System.arraycopy(samples, 0, allSamples, fileLength - header.length, samples.length);
              file.close();
              genLowResChunk(allSamples, lastChunk, ch);
            }
            repaint();
          } catch (Exception e) {
            JFLog.log(e);
          }
        }
        return;
      }
    }
    //create new chunk (after last chunk)
    cid = genChunkID();
    //create new chunk
    chunk.cid = cid;
    chunk.length = samplesLength;
    chunk.next_cid = -1;
    try {
      for(int ch=0;ch<channels;ch++) {
        byte samples[] = sliceSamples(data, ch);
        FileOutputStream fos = new FileOutputStream(project.path + "/" + tid + "/c" + cid + "-" + ch + ".dat");
        chunk.write(fos);
        fos.write(samples);
        fos.close();
        genLowResChunk(samples, chunk, ch);
      }
    } catch (Exception e) {
      JFLog.log(e);
    }
    if (list.size() > 0) {
      patchChunk(list.size()-1, cid);  //patch next_cid of last chunk to this new chunk
    }
    list.add(chunk);
    repaint();
  }
  /** Returns samples for all channels.  This is primarily for play() function. */
  public byte[] getSamples(long offset, int length) {
    ChunkHeader chunk;
    byte samples[] = new byte[length * channels * bytes];
    boolean ok = false;
//JFLog.log("getSamples(1):" + offset + "," + length);
    int pos = 0;
    for(int a=0;a<list.size();a++) {
      chunk = list.get(a);
      if (chunk.length <= offset) {
        offset -= chunk.length;
        continue;
      }
      ok = true;
      int skip;
      if (offset > 0) {
        skip = (int)offset;
        offset = 0;
      } else {
        skip = 0;
      }
      int chunkLength = (int)(chunk.length - skip);
      if (chunkLength > length) {
        chunkLength = length;
      }
//JFLog.log("getSamples(2):" + chunk.cid + "," + chunk.offset + "," + chunk.length + "," + start + "," + chunkLength);
      try {
        byte read[] = new byte[chunkLength * bytes];
        for(int ch=0;ch<channels;ch++) {
          FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat");
          fis.skip(chunkHeaderLength);
          fis.skip(skip * bytes);
          JF.readAll(fis, read, 0, read.length);
          fis.close();
          int dstpos = pos + ch * bytes;
          int srcpos = 0;
          for(int s=0;s<chunkLength;s++) {
            switch (bits) {
              case 16:
                samples[dstpos + 0] = read[srcpos + 0];
                samples[dstpos + 1] = read[srcpos + 1];
                break;
              case 32:
                samples[dstpos + 0] = read[srcpos + 0];
                samples[dstpos + 1] = read[srcpos + 1];
                samples[dstpos + 2] = read[srcpos + 2];
                samples[dstpos + 3] = read[srcpos + 3];
                break;
            }
            dstpos += channels * bytes;
            srcpos += bytes;
          }
        }
        pos += chunkLength * channels * bytes;
        length -= chunkLength;
      } catch (Exception e) {
        JFLog.log(e);
      }
      if (length == 0) break;
    }
    if (!ok) return null;
    return samples;
  }
  /** Returns samples for one channel. */
  public byte[] getSamples(long offset, int length, int ch) {
    ChunkHeader chunk;
    byte samples[] = new byte[length * bytes];
    boolean ok = false;
//JFLog.log("getSamples(1):" + offset + "," + length);
    int pos = 0;
    for(int a=0;a<list.size();a++) {
      chunk = list.get(a);
      if (chunk.length <= offset) {
        offset -= chunk.length;
        continue;
      }
      ok = true;
      int skip;
      if (offset > 0) {
        skip = (int)offset;
        offset = 0;
      } else {
        skip = 0;
      }
      int chunkLength = (int)(chunk.length - skip);
      if (chunkLength > length) {
        chunkLength = length;
      }
//JFLog.log("getSamples(2):" + chunk.cid + "," + chunk.offset + "," + chunk.length + "," + start + "," + chunkLength);
      try {
        byte read[] = new byte[chunkLength * bytes];
        FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat");
        fis.skip(chunkHeaderLength);
        fis.skip(skip * bytes);
        JF.readAll(fis, read, 0, read.length);
        fis.close();
        int dstpos = pos;
        int srcpos = 0;
        for(int s=0;s<chunkLength;s++) {
          switch (bits) {
            case 16:
              samples[dstpos + 0] = read[srcpos + 0];
              samples[dstpos + 1] = read[srcpos + 1];
              break;
            case 32:
              samples[dstpos + 0] = read[srcpos + 0];
              samples[dstpos + 1] = read[srcpos + 1];
              samples[dstpos + 2] = read[srcpos + 2];
              samples[dstpos + 3] = read[srcpos + 3];
              break;
          }
          dstpos += bytes;
          srcpos += bytes;
        }
        pos += chunkLength * bytes;
        length -= chunkLength;
      } catch (Exception e) {
        JFLog.log(e);
      }
      if (length == 0) break;
    }
    if (!ok) return null;
    return samples;
  }
  /** Sets samples for one channel. */
  public void setSamples(long offset, byte samples[], int ch) {
    ChunkHeader chunk;
    int samplesPos = 0; //in samples
    int length = samples.length / bytes;
    for(int a=0;a<list.size();a++) {
      chunk = list.get(a);
      if (chunk.length <= offset) {
        offset -= chunk.length;
        continue;
      }
      int skip;  //in samples
      if (offset > 0) {
        skip = (int)offset;
        offset = 0;
      } else {
        skip = 0;
      }
      int chunkLength = (int)(chunk.length - skip);
      if (chunkLength > length) {
        chunkLength = length;
      }
      try {
        byte allSamples[] = new byte[chunk.length * bytes];
        RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat", "rw");
        raf.seek(chunkHeaderLength);
        JF.readAll(raf, allSamples, 0, chunk.length * bytes);
        raf.seek(chunkHeaderLength + skip * bytes);
        raf.write(samples, samplesPos * bytes, chunkLength * bytes);
        System.arraycopy(samples, samplesPos * bytes, allSamples, skip * bytes, chunkLength * bytes);
        raf.close();
        genLowResChunk(allSamples, chunk, ch);
        samplesPos += chunkLength;
        length -= chunkLength;
      } catch (Exception e) {
        JFLog.log(e);
      }
      if (length == 0) break;
    }
  }
  /** Returns a channel of samples from a combined samples array. */
  public byte[] sliceSamples(byte in[], int ch) {
    int length = in.length / channels;
    int samples = length / bytes;
    byte out[] = new byte[length];
    int dstpos = 0;
    int srcpos = ch * bytes;
    for(int a=0;a<samples;a++) {
      for(int b=0;b<bytes;b++) {
        out[dstpos++] = in[srcpos++];
      }
      srcpos += (channels - 1) * bytes;  //skip other channels
    }
    return out;
  }
  public void unselectAll() {
    selectStart = selectStop = 0;
    selected = false;
    repaint();
  }
  public void mute() {
    muted = true;
    repaint();
  }
  public void unmute() {
    muted = false;
    repaint();
  }
  public Transcoder transcoder;
  public boolean transcoderSuccess;
  public File transcoderInFile;
  public String transcoderOutFile;
  public String transcoderCodec;
  public int transcoderBitRate;
  public void exportFile(String fn, boolean selection) {
    if (fn.toLowerCase().endsWith(".wav")) {
      if (exportWav(fn, selection)) JFAWT.showMessage("Notice", "Export complete");
    } else {
      //export to Wav to temp file and then convert using CodecPack
      transcoderCodec = null;
      if (fn.toLowerCase().endsWith(".flac")) {
        transcoderCodec = "flac";
      }
      if (fn.toLowerCase().endsWith(".ogg")) {
        transcoderCodec = "ogg";
      }
      if (fn.toLowerCase().endsWith(".wma")) {
        transcoderCodec = "wma";
      }
      if (fn.toLowerCase().endsWith(".mp3")) {
        AudioApp.inDialog = true;
        String bitRate = JFAWT.getString("Enter MP3 BitRate (16-384)K", "128");
        AudioApp.inDialog = false;
        if (bitRate == null) return;
        transcoderBitRate = JF.atoi(bitRate);
        if (transcoderBitRate < 16) transcoderBitRate = 16;
        if (transcoderBitRate > 384) transcoderBitRate = 384;
        transcoderCodec = "mp3";
      }
      if (transcoderCodec == null) {
        JFAWT.showError("Error", "Unsupported codec");
        return;
      }
      try {
        transcoder = new Transcoder();
        transcoder.encoder.setAudioBitRate(transcoderBitRate * 1000);
        transcoderInFile = Paths.getTempFile("temp", ".wav");
        transcoderOutFile = fn;
        if (!exportWav(transcoderInFile.getAbsolutePath(), selection)) return;
        JFTask task = new JFTask() {
          public boolean work() {
            this.setProgress(-1);  //indeterminate
            this.setTitle("Progress");
            this.setLabel("Transcoding file...");
            transcoderSuccess = transcoder.transcode(transcoderInFile.getAbsolutePath(), transcoderOutFile
              , transcoderCodec);
            return true;
          }
        };
        ProgressDialog dialog = new ProgressDialog(null, true, task);
        dialog.setAutoClose(true);
        dialog.setVisible(true);
//        transcoderInFile.delete();  //test
        if (transcoderSuccess)
          JFAWT.showMessage("Notice", "Export complete");
        else
          JFAWT.showError("Error", "Export failed");
      } catch (Exception e) {
        JFLog.log(e);
        JFAWT.showError("Error", "Export failed");
      }
    }
  }
  public static void writeWavHeader(FileOutputStream fos, int channels, int rate, int bits, int bytes, int dataSize) throws Exception {
    fos.write("RIFF".getBytes());
    JF.writeuint32(fos, dataSize + 36);
    fos.write("WAVE".getBytes());
    fos.write("fmt ".getBytes());
    JF.writeuint32(fos, 16);  //fmt chunk size
    JF.writeuint16(fos, 1);  //PCM format
    JF.writeuint16(fos, channels);
    JF.writeuint32(fos, rate);
    JF.writeuint32(fos, rate * channels * bytes);  //byte rate
    JF.writeuint16(fos, channels * bytes);  //block align
    JF.writeuint16(fos, bits);
    fos.write("data".getBytes());
    JF.writeuint32(fos, dataSize);
  }
  public boolean exportWav(String fn, boolean selection) {
    long dataSize;
    long length, sStart, sStop;
    long offset = 0;
    if (selection) {
      if (selectStart == selectStop) {
        JFAWT.showError("Error", "No selection to export");
        return false;
      }
      if (selectStop < selectStart) {
        sStart = (int)selectStop;
        sStop = (int)selectStart;
      } else {
        sStart = (int)selectStart;
        sStop = (int)selectStop;
      }
      length = (sStop - sStart + 1);
      dataSize = length * channels * bytes;
      offset = sStart;
    } else {
      dataSize = totalLength * channels * bytes;
      length = totalLength;
    }
    if (dataSize > Integer.MAX_VALUE) {
      JFAWT.showError("Error", "Output too large for WAV file");
      return false;
    }
    try {
      FileOutputStream fos = new FileOutputStream(fn);
      writeWavHeader(fos, channels, rate, bits, bytes, (int)dataSize);
//      JFLog.log("dataSize=" + dataSize);
      JFTask task = new JFTask() {
        private FileOutputStream fos;
        long length;
        long offset;
        public boolean work() {
          fos = (FileOutputStream)this.getProperty("fos");
          offset = (Long)this.getProperty("offset");
          length = (Long)this.getProperty("length");
          this.setLabel("Exporting file...");
          this.setTitle("Progress");
          long fullLength = length;
          try {
            while (length > 0) {
              this.setProgress((int)((fullLength - length) * 100 / fullLength));
              int read = 64 * 1024;
              if (read > length) read = (int)length;
              byte samples[] = getSamples(offset, read);
              if (samples == null) break;
              fos.write(samples);
              offset += read;
              length -= read;
            }
            fos.close();
          } catch (Exception e) {
            JFLog.log(e);
          }
          return true;
        }
      };
      task.setProperty("fos", fos);
      task.setProperty("offset", offset);
      task.setProperty("length", length);
      ProgressDialog dialog = new ProgressDialog(null, true, task);
      dialog.setAutoClose(true);
      dialog.setVisible(true);
      return true;
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Export Failed");
      return false;
    }
  }
  public void selectTrack(boolean unselectOthers) {
    selected = true;
    if (unselectOthers) project.selectTrack(this, muted);
    repaint();
  }
  public void setRate(int newRate) {
    rate = newRate;
    writeMainHeader();
  }

  private void calcLength() {
    //recalc totalLength
    int listSize = list.size();
    totalLength = 0;
    for(int lidx = 0;lidx<listSize;lidx++) {
      ChunkHeader chunk = list.get(lidx);
      totalLength += chunk.length;
    }
  }

  public void cut() {
    //cut selection to clipboard
    JFLog.log("cut");
    if (selectStart == selectStop) return;
    copySelection(MainPanel.clipboardPath);
    delete(true);
  }
  public void copy() {
    //copy selection to clipboard
    JFLog.log("copy");
    if (selectStart == selectStop) return;
    copySelection(MainPanel.clipboardPath);
  }
  public void paste(String path, boolean replaceSelection) {
    JFLog.log("paste");
    ClipHeader clip = new ClipHeader();
    try {
      FileInputStream fis = new FileInputStream(path + "/clip.dat");
      clip.read(fis);
      fis.close();
      if (clip.length == 0) {JFLog.log("clip is empty"); return;}
      if (clip.bits != bits) {
        JFAWT.showError("Error", "Clipboard bits doesn't match this track");
        return;
      }
      if (clip.channels != channels) {
        JFAWT.showError("Error", "Clipboard channels doesn't match this track");
        return;
      }
      if (clip.rate != rate) {
        if (!JFAWT.showConfirm("Warning", "Clipboard sample rate doesn't match this track, use anyways?")) {
          return;
        }
      }
      if (replaceSelection) {
        if (selectStart != selectStop) {
          delete(true);
        }
        undoAction2 = ACTION_PASTE;
        undoAction2OffsetStart = selectStart;
        undoAction2OffsetStop = selectStart + clip.length;
      }
      //find where selectStart is
      long offset = selectStart;
      int listSize = list.size();
      ChunkHeader chunk;
      for(int lidx=0;lidx<listSize;lidx++) {
        chunk = list.get(lidx);
        if (chunk.length <= offset) {
          offset -= chunk.length;
          continue;
        }
        if (offset == 0) {
          //insert just before this chunk
          int _first_cid = copyChunks(path, chunk.cid, lidx);
          if (lidx > 0) {
            patchChunk(lidx, _first_cid);
          } else {
            first_cid = _first_cid;
            writeMainHeader();
          }
          calcLength();
          repaint();
          return;
        } else {
          //insert somewhere in this chunk
          //split this chunk into 2 chunks @ offset
          splitChunk(lidx, (int)offset);
          lidx++;
          chunk = list.get(lidx);
          int _first_cid = copyChunks(path, chunk.cid, lidx);
          patchChunk(lidx, _first_cid);
          calcLength();
          repaint();
          return;
        }
      }
      //insert at end of track
      int _first_cid = copyChunks(path, -1, listSize);
      if (listSize > 0) {
        patchChunk(listSize-1, _first_cid);
      } else {
        first_cid = _first_cid;
        writeMainHeader();
      }
      calcLength();
      repaint();
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
  public void splitChunk(int lidx, int length1) {
    JFLog.log("splitChunk:" + lidx + "," + length1);
    try {
      ChunkHeader chunk1 = list.get(lidx);
      ChunkHeader chunk2 = new ChunkHeader();
      chunk2.length = (int)(chunk1.length - length1);
      chunk1.length = (int)length1;
      chunk2.cid = genChunkID();
      chunk2.next_cid = chunk1.next_cid;
      chunk1.next_cid = chunk2.cid;
      byte samples1[] = new byte[chunk1.length * bytes];
      byte samples2[] = new byte[chunk2.length * bytes];
      for(int ch=0;ch<channels;ch++) {
        FileInputStream fis = new FileInputStream(project.path + "/" + tid + "/c" + chunk1.cid + "-" + ch + ".dat");
        fis.skip(chunkHeaderLength);
        JF.readAll(fis, samples1, 0, samples1.length);
        JF.readAll(fis, samples2, 0, samples2.length);
        fis.close();
        FileOutputStream fos1 = new FileOutputStream(project.path + "/" + tid + "/c" + chunk1.cid + "-" + ch + ".dat");
        chunk1.write(fos1);
        fos1.write(samples1);
        fos1.close();
        genLowResChunk(samples1, chunk1, ch);
        FileOutputStream fos2 = new FileOutputStream(project.path + "/" + tid + "/c" + chunk2.cid + "-" + ch + ".dat");
        chunk2.write(fos2);
        fos2.write(samples2);
        fos2.close();
        genLowResChunk(samples2, chunk2, ch);
      }
      list.add(lidx+1, chunk2);
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
  /** Ensures all chunks are <= maxChunkSize. */
/*
  public void splitChunks() {
    for(int a=0;a<list.size();a++) {
      ChunkHeader chunk = list.get(a);
      if (chunk.length > maxChunkSize) {
        splitChunk(a, maxChunkSize);
      }
    }
  }
*/
  public void patchChunk(int lidx, int newnext_cid) {
    ChunkHeader chunk = list.get(lidx);
    try {
      //patch last chunk next_cid to cid
      chunk.next_cid = newnext_cid;
      ChunkHeader patch = new ChunkHeader();
      for(int ch=0;ch<channels;ch++) {
        RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat", "rw");
        patch.read(raf);
        raf.seek(0);
        patch.next_cid = newnext_cid;
        patch.write(raf);
        raf.close();

        raf = new RandomAccessFile(project.path + "/" + tid + "/a" + chunk.cid + "-" + ch + ".dat", "rw");
        patch.read(raf);
        raf.seek(0);
        patch.next_cid = newnext_cid;
        patch.write(raf);
        raf.close();
      }
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
  public int copyChunks(String path, int last_cid, int insertIdx) {
    JFLog.log("copyChunks:" + last_cid + "," + insertIdx);
    int _first_cid;
    try {
      int cid = genChunkID();
      _first_cid = cid;
      int clipid = 0;
      ChunkHeader chunk;
      byte samples[] = new byte[maxChunkSize * bytes];
      int next_cid;
      boolean lastChunk = false;
      do {
        chunk = new ChunkHeader();
        chunk.cid = cid;
        list.add(insertIdx++, chunk);
        if (!new File(path + "/c" + (clipid+1) + "-0.dat").exists()) {
          next_cid = last_cid;
          lastChunk = true;
        } else {
          next_cid = genChunkID();
        }
        chunk.next_cid = next_cid;
        for(int ch=0;ch<channels;ch++) {
          ChunkHeader clip = new ChunkHeader();
          FileInputStream fis = new FileInputStream(path + "/c" + clipid + "-" + ch + ".dat");
          clip.read(fis);
          JF.readAll(fis, samples, 0, clip.length * bytes);
          chunk.length = clip.length;
          FileOutputStream fos = new FileOutputStream(project.path + "/" + tid + "/c" + cid + "-" + ch + ".dat");
          chunk.write(fos);
          fos.write(samples, 0, chunk.length * bytes);
          fos.close();
          genLowResChunk(samples, chunk, ch);
        }
        cid = next_cid;
        clipid++;  //clips are always sequential
      } while (!lastChunk);
      return _first_cid;
    } catch (Exception e) {
      JFLog.log(e);
      return -1;
    }
  }
  public void delete(boolean createUndo) {
    JFLog.log("delete");
    if (selectStart == selectStop) {
      JFLog.log("nothing to delete");
      return;
    }
    long length, sStart, sStop;
    if (selectStop < selectStart) {
      sStart = selectStop;
      sStop = selectStart;
    } else {
      sStart = selectStart;
      sStop = selectStop;
    }
    if (createUndo) {
      undoAction1 = ACTION_CUT;
      undoAction2 = ACTION_NA;
      copySelection(project.path + "/undo");
      undoAction1OffsetStart = sStart;
      undoAction1OffsetStop = sStop;
    }
    selectStart = selectStop = sStart;
    project.selectStart(selectStart);
    project.selectStop(selectStop);
    length = (sStop - sStart + 1);
    long offset = sStart;
    int listSize = list.size();
    int first_idx = -1, last_idx = -1;
    for(int lidx = 0;lidx<listSize;) {
      if (length == 0) break;
      ChunkHeader chunk = list.get(lidx);
      if (chunk.length < offset) {
        offset -= chunk.length;
        lidx++;
        continue;
      }
      if (offset == 0 && chunk.length <= length) {
        //delete entire chunk
        if (first_idx == -1 && lidx != 0) first_idx = lidx-1;
        if (last_idx == -1 && chunk.length == length && lidx != listSize-1) last_idx = lidx+1;
        for(int ch=0;ch<channels;ch++) {
          new File(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat").delete();
          new File(project.path + "/" + tid + "/a" + chunk.cid + "-" + ch + ".dat").delete();
        }
        if (lidx == 0) {
          //need to patch first_cid
          if (list.size() == 1) {
            first_cid = -1;  //deleting last chunk
          } else {
            first_cid = list.get(lidx+1).cid;
          }
          writeMainHeader();
        }
        list.remove(lidx);
        listSize--;
        if (last_idx != -1) last_idx--;
        length -= chunk.length;
        continue;
      }
      if (offset > 0) {
        if (chunk.length > offset + length) {
          //delete middle part of chunk (first and last)
          int orgChunkLength = chunk.length;
          chunk.length -= length;
          for(int ch=0;ch<channels;ch++) {
            try {
              byte samples[] = new byte[orgChunkLength * bytes];
              byte newSamples[] = new byte[chunk.length * bytes];
              RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat", "rw");
              chunk.write(raf);
              JF.readAll(raf, samples, 0, orgChunkLength * bytes);
              System.arraycopy(samples, 0, newSamples, 0, (int)offset * bytes);
              System.arraycopy(samples, (int)(offset + length) * bytes, newSamples, (int)offset * bytes, (int)(orgChunkLength - offset - length) * bytes);
              raf.seek(chunkHeaderLength);
              raf.write(newSamples, 0, chunk.length * bytes);
              raf.setLength(chunkHeaderLength + chunk.length * bytes);
              raf.close();
              genLowResChunk(newSamples, chunk, ch);
            } catch (Exception e) {
              JFLog.log(e);
            }
          }
          break;
        } else {
          //delete last part of chunk (first chunk)
          if (first_idx == -1 && lidx != 0) first_idx = lidx;
          int orgChunkLength = chunk.length;
          chunk.length -= chunk.length - offset;
          for(int ch=0;ch<channels;ch++) {
            try {
              RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat", "rw");
              chunk.write(raf);
              raf.setLength(chunkHeaderLength + chunk.length * bytes);
              raf.close();

              raf = new RandomAccessFile(project.path + "/" + tid + "/a" + chunk.cid + "-" + ch + ".dat", "rw");
              chunk.write(raf);
              raf.setLength(chunkHeaderLength + chunk.length / lowRes);
              raf.close();
            } catch (Exception e) {
              JFLog.log(e);
            }
          }
          length -= orgChunkLength - offset;
          if (last_idx == -1 && length == 0 && lidx != listSize-1) last_idx = lidx;
          offset = 0;
          lidx++;
          continue;
        }
      } else {
        //delete first part of chunk (last chunk) [offset = 0]
        if (first_idx == -1 && lidx != 0) first_idx = lidx;
        if (last_idx == -1 && lidx != listSize-1) last_idx = lidx;
        int orgChunkLength = chunk.length;
        chunk.length -= length;
        for(int ch=0;ch<channels;ch++) {
          try {
            byte samples[] = new byte[orgChunkLength * bytes];
            RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk.cid + "-" + ch + ".dat", "rw");
            chunk.write(raf);
            JF.readAll(raf, samples, 0, orgChunkLength * bytes);
            System.arraycopy(samples, (int)length * bytes, samples, 0, chunk.length * bytes);
            raf.seek(chunkHeaderLength);
            raf.write(samples, 0, chunk.length * bytes);
            raf.setLength(chunkHeaderLength + chunk.length * bytes);
            raf.close();
            genLowResChunk(samples, chunk, ch);
          } catch (Exception e) {
            JFLog.log(e);
          }
        }
//        length = 0;
        lidx++;
        break;
      }
    }
    //patch first chunk.next_cid to last chunk.cid
    if (first_idx == -1 && last_idx == -1) {
      calcLength();
      repaint();
      return;
    }
    ChunkHeader chunk_first = list.get(first_idx);
    ChunkHeader chunk_last;
    if (last_idx != -1) chunk_last = list.get(last_idx); else chunk_last = null;
    for(int ch=0;ch<channels;ch++) {
      try {
        RandomAccessFile raf = new RandomAccessFile(project.path + "/" + tid + "/c" + chunk_first.cid + "-" + ch + ".dat", "rw");
        chunk_first.read(raf);
        chunk_first.next_cid = (chunk_last != null ? chunk_last.cid : -1);
        raf.seek(0);
        chunk_first.write(raf);
        raf.close();
      } catch (Exception e) {
        JFLog.log(e);
      }
    }
    calcLength();
    repaint();
  }

  public void copySelection(String toPath) {
    long length, sStart, sStop;
    if (selectStop < selectStart) {
      sStart = selectStop;
      sStop = selectStart;
    } else {
      sStart = selectStart;
      sStop = selectStop;
    }
    length = (sStop - sStart + 1);
    long offset = sStart;
    JFLog.log("copySelection:" + sStart + "-" + sStop + ":path=" + toPath);
    try {
      FileOutputStream fos = new FileOutputStream(toPath + "/clip.dat");
      ClipHeader clip = new ClipHeader();
      clip.offset = sStart;
      clip.length = length;
      clip.tid = tid;
      clip.channels = channels;
      clip.bits = bits;
      clip.bytes = bytes;
      clip.rate = rate;
      clip.write(fos);
      fos.close();
      int cid = 0;
      while (length > 0) {
        int read = maxChunkSize;
        if (read > length) read = (int)length;
        for(int ch=0;ch<channels;ch++) {
          byte samples[] = getSamples(offset, read, ch);
          fos = new FileOutputStream(toPath + "/c" + cid + "-" + ch + ".dat");
          ChunkHeader chunk = new ChunkHeader();
          chunk.cid = cid;
          chunk.length = samples.length / bytes;
          chunk.next_cid = (read == length ? 0 : cid+1);
          chunk.write(fos);
          fos.write(samples);
          fos.close();
        }
        length -= read;
      }
    } catch (Exception e) {
      JFLog.log(e);
    }
  }
  /** Returns a chunk id that is not in use. */
  public int genChunkID() {
    int cid = list.size();
    for(int a=0;a<list.size();) {
      if (list.get(a).cid == cid) {cid++; a = 0;} else {a++;}
    }
    return cid;
  }
  public void selectAll() {
    selectStart = 0;
    selectStop = totalLength-1;
    repaint();
  }

  public void createModifyUndo() {
    copySelection(project.path + "/undo");
    undoAction1 = ACTION_MODIFY;
    undoAction1OffsetStart = selectStart;
    undoAction1OffsetStop = selectStop;
  }

  public void undo() {
    switch (undoAction2) {
      case ACTION_PASTE:
        JFLog.log("undo:paste(2)");
        selectStart = undoAction2OffsetStart;
        selectStop = undoAction2OffsetStop;
        delete(false);
        selectStart = selectStop = undoAction2OffsetStart;
        break;
    }
    switch (undoAction1) {
      case ACTION_CUT:
        JFLog.log("undo:cut");
        selectStart = undoAction1OffsetStart;
        selectStop = selectStart;
        paste(project.path + "/undo", false);
        selectStart = selectStop = undoAction1OffsetStart;
        break;
      case ACTION_PASTE:
        JFLog.log("undo:paste(1)");
        selectStart = undoAction1OffsetStart;
        selectStop = undoAction1OffsetStop;
        delete(false);
        selectStart = selectStop = undoAction1OffsetStart;
        break;
      case ACTION_MODIFY:
        JFLog.log("undo:modify");
        selectStart = undoAction1OffsetStart;
        selectStop = undoAction1OffsetStop;
        delete(false);
        selectStart = selectStop = undoAction1OffsetStart;
        if (undoRate != 0) {
          //fxResample
          rate = undoRate;
          undoRate = 0;
        }
        paste(project.path + "/undo", false);
        break;
    }
    undoAction1 = ACTION_NA;
    undoAction2 = ACTION_NA;
    calcLength();
    project.deleteUndo();
  }

  private Dimension layoutSize;
  private class TrackLayout implements LayoutManager {
//    private Vector<Component> list = new Vector<Component>();
    public void addLayoutComponent(String string, Component cmp) {
//      list.add(cmp);
    }

    public void removeLayoutComponent(Component cmp) {
//      list.remove(cmp);
    }

    public Dimension preferredLayoutSize(Container c) {
      if (layoutSize == null) layoutContainer(c);
      return layoutSize;
    }

    public Dimension minimumLayoutSize(Container c) {
      return new Dimension(1,1);
    }

    public void layoutContainer(Container c) {
      int cnt = c.getComponentCount();
      if (cnt != channels) return;
      int x = project.getTracksWidth();
      int cx = 0;
      int cy = 0;
      for(int ch=0;ch<channels;ch++) {
        Component child = c.getComponent(ch);
        Dimension d = child.getPreferredSize();
        child.setBounds(cx, cy, x, d.height);
        cy += d.height;
      }
      c.setPreferredSize(new Dimension(x, cy));
      layoutSize = new Dimension(x, cy);
    }
  }

  public void info() {
    String info = "Length:" + totalLength + " (samples)\n";
    info += "Freq:" + rate + "\n";
    info += "Bits:" + bits + "\n";
    JFAWT.showMessage("Track Info", info);
  }

  public void fxAmplify() {
    if (totalLength == 0) {
      JFLog.log("totalLength=0");
      return;
    }
    if (selectStart == selectStop) selectAll();
    AudioApp.inDialog = true;
    FxAmplify fx = new FxAmplify(null, true, this);
    fx.setVisible(true);
    AudioApp.inDialog = false;
    repaint();
  }
  public void fxFadeIn() {
    if (totalLength == 0) {
      JFLog.log("totalLength=0");
      return;
    }
    if (selectStart == selectStop) selectAll();
    FxFade.fadeIn(this);
    repaint();
  }
  public void fxFadeOut() {
    if (totalLength == 0) {
      JFLog.log("totalLength=0");
      return;
    }
    if (selectStart == selectStop) selectAll();
    FxFade.fadeOut(this);
    repaint();
  }
  public void fxResample() {
    if (totalLength == 0) {
      JFLog.log("totalLength=0");
      return;
    }
    AudioApp.inDialog = true;
    FxResample fx = new FxResample(null, true, this);
    fx.setVisible(true);
    AudioApp.inDialog = false;
    repaint();
  }
  public void genTone() {
    AudioApp.inDialog = true;
    GenTone fx = new GenTone(null, true, this);
    fx.setVisible(true);
    AudioApp.inDialog = false;
    repaint();
  }
  public void genSilence() {
    AudioApp.inDialog = true;
    GenSilence fx = new GenSilence(null, true, this);
    fx.setVisible(true);
    AudioApp.inDialog = false;
    repaint();
  }

}
